/*********************************************************************
*
* Software License Agreement (BSD License)
*
*  Copyright (c) 2010, Heriot-Watt University, UK.
*  All rights reserved.
*
*  Redistribution and use in source and binary forms, with or without
*  modification, are permitted provided that the following conditions
*  are met:
*
*   * Redistributions of source code must retain the above copyright
*     notice, this list of conditions and the following disclaimer.
*   * Redistributions in binary form must reproduce the above
*     copyright notice, this list of conditions and the following
*     disclaimer in the documentation and/or other materials provided
*     with the distribution.
*   * Neither the name of the Heriot-Watt University nor the names of
*     its contributors may be used to endorse or promote products
*     derived from this software without specific prior written
*     permission.
*
*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
*  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
*  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
*  FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
*  COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
*  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
*  BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
*  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
*  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
*  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
*  ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
*  POSSIBILITY OF SUCH DAMAGE.
*
*  Author: Joel Cartwright
*
*********************************************************************/

#ifndef MATCHMAKER_CLIENT_H_
#define MATCHMAKER_CLIENT_H_

#include <boost/bind.hpp>
#include <boost/make_shared.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/locks.hpp>

#include <image_transport/camera_publisher.h>

#include <matchmaker/version.h>
#include <matchmaker/service_advertiser.h>
#include <matchmaker/service_monitor.h>
#include <matchmaker/service_query.h>

#include <matchmaker_msgs/FindServices.h>
#include <matchmaker_msgs/GetServiceStatus.h>
#include <matchmaker_msgs/ClientUpdate.h>
#include <matchmaker_msgs/ServerUpdate.h>

namespace matchmaker
{

typedef std::map<std::string, ServiceMonitor> M_StringToServiceMonitor;
typedef std::map<std::string, ServiceQuery> M_StringToServiceQuery;

/**
 * @brief Interface for advertising and discovering SOA services
 *
 * This class is used by SOA service providers and consumers, in order to
 * advertise and discover services via the SOA matchmaker server.
 */
class Client : boost::noncopyable
{
public:
  /**
   * @brief Constructor
   *
   * When a Client is created, it immediately tries to connect to the matchmaker
   * server. It will not call ros::init, so this should have been done before
   * construction. If you want a Client as a member variable in a class, you
   * might want to use ClientPtr instead and instantiate the Client like this:
\verbatim
myClient_ = boost::make_shared<matchmaker::Client>();
\endverbatim
   *
   * Note that the Client does not start its own thread, but relies on
   * ros::spin() or ros::spinOnce() being called elsewhere in the user code.
   * This is essential for it to process its message queues and timers.
   */
  Client();

  /**
   * @brief Destructor
   *
   * When a Client is destroyed, it calls shutdown on itself.
   */
  virtual ~Client();

  /**
   * @brief Cancels all advertisers, monitors, etc.
   *
   * This will cancel all advertisers and monitors the Client had with the
   * matchmaker server, and perform any necessary memory cleanup.
   */
  void shutdown();

  /**
   * @brief Test if the Client is connected to the matchmaker server
   *
   * @return true if the Client is active and the matchmaker server is available
   */
  bool isConnected();

  /** @brief Get the matchmaker version string
   *
   * @return version string
   */
  static std::string getVersionString();

  /**
   * @brief Advertise any type of SOA service by topic
   *
   * This form of the overloaded advertiseService method can be used to
   * advertise any type of SOA service, given a topic. This includes
   * ROS services and ROS actions, for which there are not specific
   * overloaded methods.
   *
   * As long as at least one active ServiceAdvertiser object is kept in scope
   * by the user, the service will continue to be advertised. To stop
   * advertising the service, the user should either destroy or call shutdown
   * on its copy (or copies) of the ServiceAdvertiser.

   * @param name service name
   * @param topic ROS service topic (not available from ServiceServer object)
   *
   * @return ServiceAdvertiser advertiser instance
   */
  ServiceAdvertiser advertiseService(const std::string& name,
      const std::string& topic);

  /**
   * @brief Advertise a ROS message publisher SOA service
   *
   * This is a more convenient form of the generic advertiseService method;
   * the topic is automatically obtained from the ros::Publisher.
   *
   * As long as at least one active ServiceAdvertiser object is kept in scope
   * by the user, the service will continue to be advertised. To stop
   * advertising the service, the user should either destroy or call shutdown
   * on its copy (or copies) of the ServiceAdvertiser.
   *
   * @param name service name
   * @param publisher ROS message publisher making up this service
   *
   * @return ServiceAdvertiser advertiser instance
   */
  ServiceAdvertiser advertiseService(const std::string& name,
      const ros::Publisher& publisher);

  /**
   * @brief Advertise a ROS message subscriber SOA service
   *
   * This is a more convenient form of the generic advertiseService method;
   * the topic is automatically obtained from the ros::Subscriber.

   * As long as at least one active ServiceAdvertiser object is kept in scope
   * by the user, the service will continue to be advertised. To stop
   * advertising the service, the user should either destroy or call shutdown
   * on its copy (or copies) of the ServiceAdvertiser.

   * @param name service name
   * @param subscriber ROS message subscriber making up this service
   *
   * @return ServiceAdvertiser advertiser instance
   */
  ServiceAdvertiser advertiseService(const std::string& name,
      const ros::Subscriber& subscriber);

  /**
   * @brief Advertise a ROS camera publisher SOA service
   *
   * This is a more convenient form of the generic advertiseService method;
   * the basic image topic is automatically obtained from the
   * image_transport::CameraPublisher.
   *
   * As long as at least one active ServiceAdvertiser object is kept in scope
   * by the user, the service will continue to be advertised. To stop
   * advertising the service, the user should either destroy or call shutdown
   * on its copy (or copies) of the ServiceAdvertiser.

   * @param name service name
   * @param publisher ROS message subscriber making up this service
   *
   * @return ServiceAdvertiser advertiser instance
   */
  ServiceAdvertiser advertiseService(const std::string& name,
      const image_transport::CameraPublisher& publisher);

  /**
   * @brief Find a service by name
   *
   * If the Client finds there is more than one matching service instance
   * available, one will arbitrarily be chosen. The returned object is a boost
   * shared_ptr. It may be freely copied and used like a pointer, and when the
   * last instance goes out of scope, the object it points to will be
   * automatically destroyed. Example use:
\verbatim
ServiceDetailsPtr details = findService(myName);
if (details) {
  std::cout << "Found service with topic:  " << details->topic << std::endl;
  ...
}
else {
  std::cout << "Failed to find service: " << myName << std::endl;
}
\endverbatim
   *
   * Note that by default this method will only find services for which
   * status.isUsable() is currently true.
   *
   * @param name service name
   * @param usableNow true to only find usable service (default=true)
   * @return valid shared_ptr on success, empty on failure
   */
  ServiceDetailsPtr findService(const std::string& name, bool usableNow = true);

  /**
   * @brief Retrieve the status of a service
   * @param id service identifier
   * @return ServiceStatus of service if found, or ServiceStatus(REMOVED) if not
   */
  ServiceStatus getServiceStatus(const std::string& id);

  /**
   * @brief Create a status monitor for a service with a callback
   *
   * The statusCallback function will be called once during creation of the
   * service monitor, and subsequently on every status change of the
   * service. If the optional forced callback interval is set, the callback
   * will also occur at the specified interval.
   *
   * As long as at least one active ServiceMonitor object is kept in scope
   * by the user, the service will continue to be monitored. To stop
   * monitoring the service, the user should either destroy or call shutdown
   * on all copies of the ServiceMonitor.
   *
   * @param service details of the service to monitor
   * @param statusCallback function to call when the service status changes
   * @param forcedCallbackInterval interval to force callback at, in seconds (default=0)
   * @return ServiceMonitor monitor instance
   */
  ServiceMonitor monitorService(const ServiceDetailsPtr& service,
      boost::function<void (const ServiceDetails& service)> statusCallback,
      float forcedCallbackInterval = 0);

  /**
   * @brief Create a status monitor for a service
   *
   * As long as at least one active ServiceMonitor object is kept in scope
   * by the user, the service will continue to be monitored. To stop
   * monitoring the service, the user should either destroy or call shutdown
   * on all copies of the ServiceMonitor.
   *
   * Use of the callback version of monitorService is recommended over
   * this version. The user must repeatedly call getStatus on the service
   * monitor with this version.
   *
   * @param service details of the service to monitor
   * @return ServiceMonitor monitor instance
   */
  ServiceMonitor monitorService(const ServiceDetailsPtr& service);

// --------------------------------------------------
// Library users can ignore code below this line.
// --------------------------------------------------

protected:
  /// Minimum period to send adverts at (also sent on change)
  static const double ADVERT_SEND_PERIOD = 1.0;

  /// Minimum period to send client update msg at (also sent on change)
  static const double CLIENT_UPDATE_MSG_SEND_PERIOD = 1.0;

  /// Grace time before the Client will complain about missing service status
  static const double ADVERT_CREATION_GRACE_PERIOD = 4.0;

  /// Period after which the Client complains about a missing matchmaker server
  static const double SERVICE_STATUS_MSG_TIMEOUT = 4.0;

  /// Tiny timeout for scheduled message sending
  static const double SCHEDULED_SEND_TIMEOUT = 0.000001;

  // Waits for a valid ros::Time (not immediately valid with simulated time)
  void waitForClock();

  // Performs a generic matchmaker service query, with some error checking.
  bool doCheckedQueryForServices(matchmaker_msgs::FindServices & srv, std::string * errorString);

  // Converts the result of a matchmaker service query to a list of services, with an optional inclusion filter.
  static void convertServicesFromQuery(const matchmaker_msgs::FindServices & srv, L_ServiceDetails * services,
      boost::function<bool (const matchmaker_msgs::Advert & o)> includeFilter = boost::lambda::constant(true));

  ServiceStatus getServiceStatusInternal(const std::string& id);

  // Callback for when a subscriber connects to our advert stream, so we can send out adverts instantly.
  void advertSubscriberConnected(const ros::SingleSubscriberPublisher& publisher);

  void sendAdvertsCallback(const ros::TimerEvent& event);

  void sendAllAdverts();

  void sendOneAdvert(ServiceAdvertiser* service);

  void sendCancellationAdverts();

  void serverUpdatePublisherConnected(const ros::SingleSubscriberPublisher& publisher);

  void scheduleSendClientUpdateMsg();

  void sendClientUpdateMsgCallback(const ros::TimerEvent& event);

  void sendClientUpdateMsg();

  void fillClientUpdateMonitors(matchmaker_msgs::ClientUpdate& msg);

  void fillClientUpdateQueries(matchmaker_msgs::ClientUpdate& msg);

  void handleServerUpdateMsg(const matchmaker_msgs::ServerUpdateConstPtr& msg);

  void handleMonitorUpdates(const matchmaker_msgs::ServerUpdateConstPtr& msg,
      L_ServiceMonitor& monitorCallbackList);

  void handleQueryUpdates(const matchmaker_msgs::ServerUpdateConstPtr& msg,
      L_ServiceQuery& queryCallbackList);

  static bool testAdvertStatusUsable(const matchmaker_msgs::Advert & o);

  /** Mutex to control access to singletonActive_ variable. */
  static boost::mutex singletonMutex_;
  /** Flag to know if there is an active Client in this process. */
  static bool singletonActive_;

  /** Flag to know if the Client is fully initialised */
  bool active_;

  /**
   * Big mutex for the class, to be locked on entry to any public methods and callbacks
   * createLocalAdvertiser also locks it, to avoid locking in each advertiseService(...) method.
   */
  boost::mutex classMutex_;

  /**
   * Mutex just for the advert publisher
   * This is locked within sendOneAdvert() to allow calling by ServiceMonitor class.
   */
  boost::mutex advertPubMutex_;

  /** ROS node handle */
  ros::NodeHandlePtr nh_;

  /** Local service records */
  L_ServiceAdvertiser localServices_;

  /** Service monitor records */
  M_StringToServiceMonitor serviceMonitorMap_;

  /** Service query records */
  M_StringToServiceQuery serviceQueryMap_;

  /** Client for finding services with the matchmaker */
  ros::ServiceClient findServicesClient_;

  /** Client for getting status of services from the matchmaker */
  ros::ServiceClient serviceStatusClient_;

  /** Publisher for sending service advertisements to the matchmaker */
  ros::Publisher advertPub_;

  /** Timer to trigger sending service advertisements to the matchmaker */
  ros::Timer advertSendTimer_;

  /** Publisher for sending monitor messages to the matchmaker */
  ros::Publisher clientUpdatePub_;

  /** Timer to trigger sending monitor messages to the matchmaker */
  ros::Timer clientUpdateSendTimer_;

  /** One shot timer to send a monitor message to the matchmaker after creating monitors or queries */
  ros::Timer clientUpdateSendOneshotTimer_;

  /** Subscriber for receiving service status messages from the matchmaker */
  ros::Subscriber serverUpdateSub_;

  /** Last time a service status message was received from the matchmaker */
  ros::Time lastServiceStatusMsgTime_;

  bool serviceStatusReceivedOnce_;
};

typedef boost::shared_ptr<Client> ClientPtr;

}

#endif /* MATCHMAKER_CLIENT_H_ */
